use std::fmt::{self, Write};

use craftdig_core::registrar::BlockRegistry;
use craftdig_datamodel::{block::model::Collider, world::WorldView};
use craftdig_math::{
    check_aabb_collision, Axis, BlkCoord, Direction, RawCollision, Vec3f, Vec3i, AABB,
};

const SEARCH_BUFFER_BOUNDS: i32 = 1;

#[derive(Copy, Clone, Debug)]
pub struct TerrainCollision {
    /// The block that was collided with.
    block: BlkCoord,

    /// The raw collision data.
    col: RawCollision,
}

impl TerrainCollision {
    pub fn block(&self) -> BlkCoord {
        self.block
    }

    pub fn col(&self) -> RawCollision {
        self.col
    }
}

/// Checks for an entity colliding against terrain, returning a list of raw collisions.
pub fn check_entity_terrain_collision(
    epos: Vec3f,
    eaabb: &AABB,
    wv: &impl WorldView,
    reg: &BlockRegistry,
) -> Vec<TerrainCollision> {
    let world_space_bb = eaabb.offset(epos);
    let sb = compute_search_bounds(&world_space_bb);

    let mut tcols = Vec::new();

    // Iterate through all the blocks in the area, fetch their AABBs, etc.
    // TODO maybe convert this to a iterator chain so we don't have to alloc the terrain collisions?
    for x in sb.base.x()..=sb.far.x() {
        for y in sb.base.y()..=sb.far.y() {
            for z in sb.base.z()..=sb.far.z() {
                let bc = BlkCoord::new(x, y, z);
                if let Some(rc) = check_entity_block_collision(&world_space_bb, bc, wv, reg) {
                    tcols.push(TerrainCollision { block: bc, col: rc });
                }
            }
        }
    }

    tcols
}

fn check_entity_block_collision(
    ewsbb: &AABB,
    blkpos: BlkCoord,
    wv: &impl WorldView,
    reg: &BlockRegistry,
) -> Option<RawCollision> {
    let blkid = wv.get_block_id(blkpos)?;

    // Fast return for air.
    if blkid.is_zero() {
        return None;
    }

    let Some(block_data) = reg.get_by_id(blkid) else {
        eprintln!("craftdig: [WARN] unknown block {blkid:?} when doing collision check");
        return None;
    };

    let block_col = block_data.tydata().model().collider();
    let aabb = match block_col {
        // This might not trigger that often since air has an early return above.
        Collider::Empty => return None,
        Collider::Cubic(aabb) => aabb,
    };

    let block_wsbb = aabb.offset(blkpos.into());
    check_aabb_collision(ewsbb, &block_wsbb)
}

struct SearchBounds {
    base: Vec3i,
    far: Vec3i,
}

fn compute_search_bounds(aabb: &AABB) -> SearchBounds {
    let buffer = SEARCH_BUFFER_BOUNDS;
    let nx = aabb.base().x().floor() as i32 - buffer;
    let ny = aabb.base().y().floor() as i32 - buffer;
    let nz = aabb.base().z().floor() as i32 - buffer;
    let fc = aabb.get_far_corner();
    let mx = fc.x().ceil() as i32 + buffer;
    let my = fc.y().ceil() as i32 + buffer;
    let mz = fc.z().ceil() as i32 + buffer;
    let base = Vec3i::new(nx, ny, nz);
    let far = Vec3i::new(mx, my, mz);
    SearchBounds { base, far }
}

/// Bitmap representing the directions that a subject collided with some number
/// of targets.
#[derive(Copy, Clone)]
pub struct CollisionDirMask(u8);

impl CollisionDirMask {
    pub fn new_empty() -> Self {
        Self(0)
    }

    /// Returns if there was any collision.
    pub fn has_any(&self) -> bool {
        self.0 != 0
    }

    /// Sets the bit indicating that we collided with something while travelling in that direction.
    pub fn set_dir(&mut self, d: Direction) {
        self.0 |= 1 << d.to_index();
    }

    /// Checks the bit indicating that we collided with something while travelling in that direction.
    pub fn has_dir(&self, d: Direction) -> bool {
        (self.0 >> d.to_index()) & 1 == 1
    }

    /// Checks the bits for the direction in either axis.
    pub fn has_axis(&self, a: Axis) -> bool {
        self.has_dir(a.positive_dir()) || self.has_dir(a.negative_dir())
    }

    /// Returns a vec we can scale the velocity of the subject by to negate its
    /// velocity in only the axes that there was a collision and leave the
    /// others untouched.
    pub fn compute_negating_scale(&self) -> Vec3f {
        let sx = !self.has_axis(Axis::X) as u8;
        let sy = !self.has_axis(Axis::Y) as u8;
        let sz = !self.has_axis(Axis::Z) as u8;
        Vec3f::new(sx as f32, sy as f32, sz as f32)
    }
}

impl fmt::Debug for CollisionDirMask {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut ent = false;

        // Go through each direction and write it if it's there.
        for d in Direction::array() {
            if self.has_dir(d) {
                if ent {
                    f.write_char('|')?;
                }
                d.fmt(f)?;
                ent = true;
            }
        }

        // If none have been written, print none instead.
        if !ent {
            f.write_str("None")?;
        }

        Ok(())
    }
}

#[derive(Copy, Clone, Debug)]
pub struct CollisionSummary {
    nudge: Vec3f,
    mask: CollisionDirMask,
}

impl CollisionSummary {
    pub fn nudge(&self) -> Vec3f {
        self.nudge
    }

    pub fn mask(&self) -> CollisionDirMask {
        self.mask
    }
}

/// Computes the effective nudge of several collisions.  If there's collisions
/// in opposing directions, both are ignored.
pub fn compute_collision_summary(
    collisions: impl Iterator<Item = RawCollision>,
) -> CollisionSummary {
    let mut dirs = [None; 6];
    let mut mask = CollisionDirMask::new_empty();

    for c in collisions {
        mask.set_dir(c.travelling_dir());

        let d = -c.travelling_dir();
        let amt = c.penetration_depth();
        let dv = &mut dirs[d.to_index()];

        if let Some(pv) = dv {
            if amt > *pv {
                *dv = Some(amt);
            }
        } else {
            *dv = Some(amt)
        }
    }

    let x = match (dirs[0], dirs[1]) {
        (Some(v), None) => v,
        (None, Some(v)) => -v,
        _ => 0.0,
    };

    let y = match (dirs[2], dirs[3]) {
        (Some(v), None) => v,
        (None, Some(v)) => -v,
        _ => 0.0,
    };

    let z = match (dirs[4], dirs[5]) {
        (Some(v), None) => v,
        (None, Some(v)) => -v,
        _ => 0.0,
    };

    CollisionSummary {
        nudge: Vec3f::new(x, y, z),
        mask,
    }
}
